feat(audio): engine availability control and external call system session mode#1127
Conversation
…sion mode Adds the two switches needed for CallKit coordination, mirroring the Swift SDK pattern: - AudioManager.setEngineAvailability/getEngineAvailability with a LiveKit-owned AudioEngineAvailability type. This is the highest priority gate over anything that may start the audio engine. Requests made while unavailable are honored once availability allows, so apps can connect and publish inside CallKit action handlers and let the engine start in provider(didActivate:). No-op on non-Apple platforms. - AudioSessionManagementMode.externalCallSystem: LiveKit keeps configuring the session from engine lifecycle but never activates or deactivates it, since the external call system (CallKit) owns activation timing. Named platform-neutrally. On Android it currently behaves like manual, reserved to also stand down audio-focus management when Telecom integration lands. - Native LiveKitPlugin.setEngineAvailability static API so an AppDelegate can gate audio before the Flutter engine exists (CallKit killed-state wake). The value is stored and applied at plugin registration. Implemented entirely on LiveKit's own plugin/channel (the ADM API is public in the WebRTC-SDK pod), so no flutter_webrtc release is needed.
On Android the mode currently behaves like automatic, not manual. The Android session configure and speakerphone paths still run, since the activation flag only exists on iOS. This is the intended interim behavior: a cross-platform app sets the mode once and Android keeps normal session management until Telecom integration lands.
- setInitialAudioSessionOptions now also seeds under externalCallSystem, which drives configuration from engine lifecycle like automatic mode. Previously the initialize-time options were silently dropped. - Document that setEngineAvailability deliberately propagates platform errors, unlike the other Native methods. A failed availability change means the engine may run outside the intended CallKit window, so the error must reach the caller. Mirrors the Swift SDK's throwing API.
There was a problem hiding this comment.
🟡 Audio session configuration not re-applied when switching from manual to external-call-system mode
The mode transition check (mode == AudioSessionManagementMode.automatic at lib/src/audio/audio_manager.dart:240) only re-applies the audio configuration when switching to automatic, so switching from manual to externalCallSystem while the engine is running leaves stale native configuration in place.
Impact: An app that switches from manual to external-call-system mode mid-session will keep the old audio session configuration until the next engine lifecycle event, causing incorrect audio routing or category.
Mechanism: the re-apply condition doesn't account for externalCallSystem being an automatic-configuration mode
Before this PR there were only two modes (automatic and manual), so the condition previousMode != automatic && mode == automatic correctly covered the only transition that needed a re-apply. With the new externalCallSystem mode, _isAutomaticConfigurationEnabled returns true for both automatic and externalCallSystem (lib/src/audio/audio_manager.dart:104). But the re-apply guard at line 240 still only checks for mode == AudioSessionManagementMode.automatic, missing the manual → externalCallSystem transition.
The fix should check whether the new mode is any automatic-configuration mode, e.g.:
if (previousMode == AudioSessionManagementMode.manual && _isAutomaticConfigurationEnabled) {
or equivalently check both automatic and externalCallSystem.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Yea I think that's expected, mid-session switching is not really supported.
- The Dart channel path now records into the pending engine availability, so both the native static and the channel keep the latest intent. A later plugin registration (a second Flutter engine in the same process) re-applies the pending value and must not lag behind a Dart-side update. - Document that deactivateAudioSession is disabled under externalCallSystem on all platforms, and scope the Android 'behaves like automatic' description to session configuration.
Apps that set the gate know what they set. Dropping the getter halves the exposed experimental surface. The native adm.engineAvailability property remains readable from Swift for debugging.
…ystems setConfiguration(_:active:) always forwards its active flag to setActive, verified in WebRTC-SDK m144 (RTCAudioSession+Configuration.mm line 34 and 129). Passing active: false therefore deactivated the session CallKit had just activated on every engine lifecycle event, silencing the call. Use the configure-only setConfiguration(_:) variant when session activation belongs to the external call system.
The overrideOutputAudioPort call was guarded by isActive, which is the activation ownership intent, not the actual session state. Under externalCallSystem the session is active during a CallKit call, yet the guard made setSpeakerOutputPreferred(force: true) a silent no-op. Apply the override for the playAndRecord category regardless of ownership and tolerate failures while the session is not active yet, where the next engine lifecycle event re-applies it.
…em mode applyOptionsForConnect acquires the Android audio session in every mode except manual, but the disconnect path still gated the release on automatic mode only. Under externalCallSystem, Android kept audio focus and communication mode held after leaving a room. Mirror the acquire predicate so acquire and release stay symmetric.
…eality The getter reports whether LiveKit applies native audio configuration. That is true in every mode except manual, so externalCallSystem should not read as disabled.
Description
Adds the two switches needed for CallKit coordination, mirroring the Swift SDK pattern (
AudioManager.setEngineAvailabilityandAudioSessionEngineObserver.isAutomaticConfigurationEnabled).Engine availability
This is the highest priority gate over anything that may start the audio engine. Requests made while unavailable are honored once availability allows, so apps can call
room.connectandsetMicrophoneEnabled(true)inside CallKit action handlers and let the engine start when CallKit activates the session. No-op on non-Apple platforms, always safe from cross-platform code.External call system session mode
LiveKit keeps configuring the session category/mode from engine lifecycle like automatic mode, but never calls
setActivein either direction. CallKit owns activation timing. This improves on the Swift example flow, where the app must set the category itself insidedidActivate.The mode is named platform-neutrally on purpose. On Android it currently behaves like manual mode, and the same mode is reserved to stand down LiveKit's audio-focus and routing management when Telecom (
androidx.core.telecom) integration lands, since the Telecom framework owns routing for registered calls.Native early gating
LiveKitPlugin.setEngineAvailability(isInputAvailable:isOutputAvailable:)is a public static so an AppDelegate can gate audio before the Flutter engine exists (CallKit killed-state wake). The value is stored and applied at plugin registration, after the engine observer is installed and before any audio operation can start the engine.Implementation notes
RTCAudioDeviceModule.setEngineAvailability) is public in the WebRTC-SDK pod, so no flutter_webrtc change or release is needed.isSessionActivationEnabled) threads through the three activation sites: managed configure, cached configure, and the engine-disable deactivation path.deactivateAudioSession()is a guarded no-op underexternalCallSystem.Testing
dart analyze lib testclean, all 344 tests pass (4 new tests for channel args and the external-mode deactivation guard).Follow-ups
androidx.core.telecom) integration behind the sameexternalCallSystemmode.